﻿// <copyright file="TTableStorageQueryTranslator.cs" company="N/A">
//     Copyright (c) Nicholas Barrett and Benjamin Kasten.  All rights reserved.
//
//     This program is free software; you can redistribute it and/or modify it
//     under the terms of the GNU General Public License as published by the
//     Free Software Foundation; either version 2 of the License, or (at your
//     option) any later version.
//
//     This program is distributed in the hope that it will be useful, but
//     WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
//     Public License for more details.
//
//     You should have received a copy of the GNU General Public License along
//     with this program; if not, write to the Free Software Foundation, 
//     Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
// </copyright>

namespace BetterAzureTableStorage
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Text;

    /// <summary>
    /// Translates LINQ expressions into Table Storage query strings.
    /// </summary>
    public class TTableStorageQueryTranslator : ExpressionVisitor
    {
        public LambdaExpression SelectNewExpression { get; private set; }

        public IList<string> ExpansionPaths { get; private set; }

        /// <summary>
        /// Gets or sets the StringBuilder used to create the translated query string
        /// </summary>
        private StringBuilder Builder { get; set; }

        private TTableStorageInfo TableInfo { get; set; }

        private bool fAddedDiscriminator;

        private int fWherePosition = -1;

        private StringBuilder WhereBuilder { get; set; }

        private StringBuilder QueryBuilder { get; set; }

        /// <summary>
        /// Translates the specified expression into a Table Storage query.
        /// </summary>
        /// <param name="expression">The expression to be translated.</param>
        /// <param name="renameProperty">The function called to determine a property's field name based on the property info.</param>
        /// <param name="tableInfo">The table info.</param>
        /// <returns>
        /// A query string suitable for use in a table storage query.
        /// </returns>
        public string Translate(Expression expression, Func<PropertyInfo, string> renameProperty, TTableStorageInfo tableInfo)
        {
            this.ExpansionPaths = new List<string>();

            this.TableInfo = tableInfo;
            this.RenameProperty = renameProperty;

            this.WhereBuilder = new StringBuilder();

            this.QueryBuilder = new StringBuilder();
            this.QueryBuilder.Append("()");

            this.Builder = this.QueryBuilder;

            this.Visit(expression);

            if (!this.fAddedDiscriminator && !String.IsNullOrEmpty(this.DiscriminatorFilter))
            {
                this.Builder.Append(this.Builder.ToString().Contains('?') ? "&" : "?");
                this.Builder.Append("$filter=");
                this.Builder.Append(this.DiscriminatorFilter);
            }

            if (this.fWherePosition != -1)
            {
                this.Builder.Insert(this.fWherePosition, this.WhereBuilder.ToString());
            }

            return Uri.EscapeUriString(this.Builder.ToString());
        }

        private string DiscriminatorFilter
        {
            get
            {
                if (this.TableInfo != null && this.TableInfo.ConditionalValue != null)
                {
                    return "Discriminator eq " + this.TableInfo.ConditionalValue;
                }

                return String.Empty;
            }
        }

        public string TranslatedText
        {
            get
            {
                return Uri.EscapeUriString(this.Builder.ToString());
            }
        }

        public override Expression Visit(Expression node)
        {
            switch ((TTableStorageExpressionType)node.NodeType)
            {
                case TTableStorageExpressionType.Expand:
                    return this.VisitExpand((TExpandExpression)node);
                default:
                    return base.Visit(node);
            }
        }

        /// <summary>
        /// Visits the expand expression.
        /// </summary>
        /// <param name="node">The expand node.</param>
        /// <returns>
        /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
        /// </returns>
        public Expression VisitExpand(TExpandExpression node)
        {
            foreach (string path in node.Path.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                this.ExpansionPaths.Add(path.Trim());
            }

            return this.Visit(node.Left);
        }

        public Expression VisitSelectMany(MethodCallExpression node)
        {
            var operandBody = ((LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand).Body;
            if (operandBody.GetType() == typeof(UnaryExpression))
            {
                this.SelectPath = ((MemberExpression)((UnaryExpression)((LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand).Body).Operand).Member.Name;
            }
            else
            {
                this.SelectPath = ((MemberExpression)((LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand).Body).Member.Name;
            }

            return node;
        }

        public Expression VisitSelect(MethodCallExpression node)
        {
            LambdaExpression le = (LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand;
            Expression body = le.Body;

            if (body is MemberInitExpression)
            {
                this.SelectNewExpression = le;
            }
            else if (body is MemberExpression)
            {
                this.SelectPath = ((MemberExpression)body).Member.Name;
            }

            return node;
        }

        /// <summary>
        /// Visits the children of the <see cref="T:System.Linq.Expressions.MethodCallExpression"/>.
        /// </summary>
        /// <param name="node">The MethodCall expression node which will be visited along with its children.</param>
        /// <returns>
        /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
        /// </returns>
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            foreach (Expression expression in node.Arguments.Where(arg => arg.NodeType == ExpressionType.Call || (int)arg.NodeType == (int)TTableStorageExpressionType.Expand))
            {
                this.Visit(expression);
            }

            if (node.Method.DeclaringType == typeof(Queryable))
            {
                if (node.Method.Name == "Take")
                {
                    if (this.fWherePosition != -1)
                    {
                        this.Builder.Append("&");
                    }
                    else
                    {
                        this.Builder.Append("?");
                    }

                    this.Builder.Append("$top=");
                    this.Visit(node.Arguments[1]);
                }

                if (node.Method.Name == "Where")
                {
                    if (this.fWherePosition == -1)
                    {
                        this.Builder.Append("?$filter=");
                        this.fWherePosition = this.Builder.Length;
                    }
                    else
                    {
                        this.WhereBuilder.Append(" and ");
                    }

                    StringBuilder storedBuilder = this.Builder;
                    this.Builder = this.WhereBuilder;

                    ////this.Visit(node.Arguments[0]);

                    LambdaExpression lambda = (LambdaExpression)StripQuotes(node.Arguments[1]);
                    this.Visit(lambda.Body);

                    if (!String.IsNullOrEmpty(this.DiscriminatorFilter))
                    {
                        this.WhereBuilder.Append(" and ");

                        this.WhereBuilder.Append(this.DiscriminatorFilter);
                        this.fAddedDiscriminator = true;
                    }

                    this.Builder = storedBuilder;
                }

                if (node.Method.Name == "Select")
                {
                    return this.VisitSelect(node);
                }

                if (node.Method.Name == "SelectMany")
                {
                    return this.VisitSelectMany(node);
                }

            }
            else if (node.Type == typeof(string))
            {
                if (node.Method != null && (
                    node.Method.Name == "ToLower" ||
                    node.Method.Name == "ToUpper" ||
                    node.Method.Name == "Trim"
                    ))
                {

                    this.Builder.AppendFormat("{0}(", node.Method.Name.ToLower());
                    string result;
                    var propExt = node.Object as MemberExpression;

                    if (propExt != null && propExt.Expression.NodeType == ExpressionType.Parameter)
                    {
                        result = propExt.Member.Name;
                    }
                    else
                    {
                        LambdaExpression le = (LambdaExpression)Expression.Lambda(node.Object);
                        Delegate fn = le.Compile();
                        result = string.Format("'{0}'", fn.DynamicInvoke(null).ToString());
                    }

                    this.Builder.AppendFormat("{0}", result);
                    this.Builder.Append(")");
                }
                else
                {
                    LambdaExpression le = (LambdaExpression)Expression.Lambda(node);
                    Delegate fn = le.Compile();
                    this.Builder.AppendFormat("'{0}'", fn.DynamicInvoke(null));
                }
            }
            else
                throw new NotSupportedException(string.Format("The method '{0}' is not supported", node.Method.Name));

            return node;
        }


        /// <summary>
        /// Visits the children of the <see cref="T:System.Linq.Expressions.BinaryExpression"/>.
        /// </summary>
        /// <param name="node">The Binary expression to visit.</param>
        /// <returns>
        /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
        /// </returns>
        protected override Expression VisitBinary(BinaryExpression node)
        {
            this.Builder.Append("(");

            if (node.Right.Type == typeof(Guid) && node.Right.NodeType == ExpressionType.MemberAccess &&
                (node.Right as MemberExpression).Expression != null &&
                (node.Right as MemberExpression).Expression.NodeType == ExpressionType.Parameter)
            {
                var propExp = node.Right as MemberExpression;

                //// TODO: this should not be hardcoded!  need to determine if an alias of a different type was used
                //// in the original expression.
                if (propExp == null || propExp.Member == null || (propExp.Member.Name != "ID" && propExp.Member.Name != "BusinessID"))
                {
                    this.Builder.Append("guid");
                }
            }

            this.Visit(node.Left);

            switch (node.NodeType)
            {
                case ExpressionType.AndAlso:
                    this.Builder.Append(" and ");
                    break;
                case ExpressionType.OrElse:
                    this.Builder.Append(" or ");
                    break;
                case ExpressionType.Equal:
                    this.Builder.Append(" eq ");
                    break;
                case ExpressionType.NotEqual:
                    this.Builder.Append(" ne ");
                    break;
                case ExpressionType.LessThan:
                    this.Builder.Append(" lt ");
                    break;
                case ExpressionType.LessThanOrEqual:
                    this.Builder.Append(" le ");
                    break;
                case ExpressionType.GreaterThan:
                    this.Builder.Append(" gt ");
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    this.Builder.Append(" ge ");
                    break;
                default:
                    throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", node.NodeType));
            }

            if (node.Left.Type == typeof(Guid) && node.Left.NodeType == ExpressionType.MemberAccess &&
                (node.Left as MemberExpression).Expression.NodeType == ExpressionType.Parameter)
            {
                var propExp = node.Left as MemberExpression;

                //// TODO: this should not be hardcoded!  need to determine if an alias of a different type was used
                //// in the original expression.
                if (propExp == null || propExp.Member == null || (propExp.Member.Name != "ID" && propExp.Member.Name != "BusinessID"))
                {
                    this.Builder.Append("guid");
                }
            }

            this.Visit(node.Right);
            this.Builder.Append(")");
            return node;
        }

        /// <summary>
        /// Visits the children of the <see cref="T:System.Linq.Expressions.NewExpression"/>.
        /// </summary>
        /// <param name="node">The expression to visit.</param>
        /// <returns>
        /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
        /// </returns>
        protected override Expression VisitNew(NewExpression node)
        {
            LambdaExpression le = Expression.Lambda(node);
            Delegate fn = le.Compile();

            object value = fn.DynamicInvoke(null);

            this.Builder.Append("'");
            this.Builder.Append(value.ToString());
            this.Builder.Append("'");

            return node;
        }

        /// <summary>
        /// Visits the <see cref="T:System.Linq.Expressions.ConstantExpression"/>.
        /// </summary>
        /// <param name="node">The Constant expression to visit.</param>
        /// <returns>
        /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
        /// </returns>
        protected override Expression VisitConstant(ConstantExpression node)
        {
            if (node.Value == null)
            {
                throw new NotImplementedException();
            }

            switch (Type.GetTypeCode(node.Value.GetType()))
            {
                case TypeCode.Boolean:
                    this.Builder.Append(((bool)node.Value) ? "true" : "false");
                    break;
                case TypeCode.String:
                    this.Builder.Append("'");
                    this.Builder.Append(node.Value);
                    this.Builder.Append("'");
                    break;
                case TypeCode.Object:
                    if (node.Value is Guid)
                    {
                        this.Builder.Append("'");
                        this.Builder.Append(node.Value.ToString());
                        this.Builder.Append("'");
                    }
                    else if (!(node.Value is IQueryable))
                    {
                        throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", node.Value));
                    }

                    break;
                default:
                    this.Builder.Append(node.Value);
                    break;
            }

            return node;
        }

        /// <summary>
        /// Visits the children of the <see cref="T:System.Linq.Expressions.MemberExpression"/>.
        /// </summary>
        /// <param name="node">The Member expression to visit.</param>
        /// <returns>
        /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
        /// </returns>
        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression != null && node.Expression.NodeType == ExpressionType.Parameter)
            {
                this.Builder.Append(this.RenameProperty((PropertyInfo)node.Member));
            }
            else if (
                        (node.Expression != null && node.Expression.NodeType == ExpressionType.MemberAccess) ||
                        node.NodeType == ExpressionType.MemberAccess
                    )
            {
                LambdaExpression le = Expression.Lambda(node);
                Delegate fn = le.Compile();

                object value = fn.DynamicInvoke(null);

                if (value is Boolean)
                {
                    this.Builder.Append((bool)value ? "true" : "false");
                }
                else
                {
                    this.Builder.Append("'");
                    this.Builder.Append(value.ToString());
                    this.Builder.Append("'");
                }
            }

            return node;
        }

        /// <summary>
        /// Gets or sets the function to lookup the field name of a property.
        /// </summary>
        public Func<PropertyInfo, string> RenameProperty { get; set; }

        /// <summary>
        /// Strips the quote expressions.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <returns>A quoteless expression.</returns>
        private static Expression StripQuotes(Expression expression)
        {
            while (expression.NodeType == ExpressionType.Quote)
            {
                expression = ((UnaryExpression)expression).Operand;
            }

            return expression;
        }

        public string SelectPath { get; set; }
    }
}
